// Copyright Epic Games, Inc. All Rights Reserved.

#include "zenutil/basicfile.h"

#include <zencore/compactbinary.h>
#include <zencore/except.h>
#include <zencore/filesystem.h>
#include <zencore/fmtutils.h>
#include <zencore/testing.h>
#include <zencore/testutils.h>

#if ZEN_PLATFORM_WINDOWS
#	include <zencore/windows.h>
#else
#	include <fcntl.h>
#	include <sys/file.h>
#	include <sys/stat.h>
#	include <unistd.h>
#endif

#include <fmt/format.h>
#include <gsl/gsl-lite.hpp>

namespace zen {

BasicFile::~BasicFile()
{
	Close();
}

void
BasicFile::Open(const std::filesystem::path& FileName, Mode Mode)
{
	std::error_code Ec;
	Open(FileName, Mode, Ec);

	if (Ec)
	{
		throw std::system_error(Ec, fmt::format("failed to open file '{}', mode: {:x}", FileName, uint32_t(Mode)));
	}
}

void
BasicFile::Open(const std::filesystem::path& FileName, Mode InMode, std::error_code& Ec)
{
	Ec.clear();

	Mode Mode = InMode & Mode::kModeMask;

#if ZEN_PLATFORM_WINDOWS
	DWORD dwCreationDisposition = 0;
	DWORD dwDesiredAccess		= 0;
	switch (Mode)
	{
		case Mode::kRead:
			dwCreationDisposition |= OPEN_EXISTING;
			dwDesiredAccess |= GENERIC_READ;
			break;
		case Mode::kWrite:
			dwCreationDisposition |= OPEN_ALWAYS;
			dwDesiredAccess |= (GENERIC_READ | GENERIC_WRITE);
			break;
		case Mode::kDelete:
			dwCreationDisposition |= OPEN_ALWAYS;
			dwDesiredAccess |= (GENERIC_READ | GENERIC_WRITE | DELETE);
			break;
		case Mode::kTruncate:
			dwCreationDisposition |= CREATE_ALWAYS;
			dwDesiredAccess |= (GENERIC_READ | GENERIC_WRITE);
			break;
		case Mode::kTruncateDelete:
			dwCreationDisposition |= CREATE_ALWAYS;
			dwDesiredAccess |= (GENERIC_READ | GENERIC_WRITE | DELETE);
			break;
	}

	const DWORD dwShareMode = FILE_SHARE_READ | (EnumHasAllFlags(InMode, Mode::kPreventWrite) ? 0 : FILE_SHARE_WRITE) |
							  (EnumHasAllFlags(InMode, Mode::kPreventDelete) ? 0 : FILE_SHARE_DELETE);
	const DWORD	 dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL;
	const HANDLE hTemplateFile		  = nullptr;
	const HANDLE FileHandle			  = CreateFile(FileName.c_str(),
										   dwDesiredAccess,
										   dwShareMode,
										   /* lpSecurityAttributes */ nullptr,
										   dwCreationDisposition,
										   dwFlagsAndAttributes,
										   hTemplateFile);

	if (FileHandle == INVALID_HANDLE_VALUE)
	{
		Ec = MakeErrorCodeFromLastError();

		return;
	}
#else
	int OpenFlags = O_CLOEXEC;
	switch (Mode)
	{
		case Mode::kRead:
			OpenFlags |= O_RDONLY;
			break;
		case Mode::kWrite:
		case Mode::kDelete:
			OpenFlags |= (O_RDWR | O_CREAT);
			break;
		case Mode::kTruncate:
		case Mode::kTruncateDelete:
			OpenFlags |= (O_RDWR | O_CREAT | O_TRUNC);
			break;
	}

	int Fd = open(FileName.c_str(), OpenFlags, 0666);
	if (Fd < 0)
	{
		Ec = MakeErrorCodeFromLastError();
		return;
	}
	if (Mode != Mode::kRead)
	{
		fchmod(Fd, 0666);
	}

	void* FileHandle = (void*)(uintptr_t(Fd));
#endif

	m_FileHandle = FileHandle;
}

void
BasicFile::Open(const std::filesystem::path& FileName, Mode Mode, std::function<bool(std::error_code& Ec)>&& RetryCallback)
{
	std::error_code Ec;
	Open(FileName, Mode, Ec);
	while (Ec && RetryCallback(Ec))
	{
		Ec.clear();
		Open(FileName, Mode, Ec);
	}
	if (Ec)
	{
		throw std::system_error(Ec, fmt::format("failed to open file '{}', mode: {:x}", FileName, uint32_t(Mode)));
	}
}

void
BasicFile::Close()
{
	if (m_FileHandle)
	{
#if ZEN_PLATFORM_WINDOWS
		::CloseHandle(m_FileHandle);
#else
		int Fd = int(uintptr_t(m_FileHandle));
		close(Fd);
#endif
		m_FileHandle = nullptr;
	}
}

IoBuffer
BasicFile::ReadRange(uint64_t FileOffset, uint64_t ByteCount)
{
	return IoBufferBuilder::MakeFromFileHandle(m_FileHandle, FileOffset, ByteCount);
}

void
BasicFile::Read(void* Data, uint64_t BytesToRead, uint64_t FileOffset)
{
	const uint64_t MaxChunkSize = 2u * 1024 * 1024 * 1024;

	while (BytesToRead)
	{
		const uint64_t NumberOfBytesToRead = Min(BytesToRead, MaxChunkSize);
		int32_t		   Error			   = 0;
		size_t		   BytesRead		   = 0;

#if ZEN_PLATFORM_WINDOWS
		OVERLAPPED Ovl{};

		Ovl.Offset	   = DWORD(FileOffset & 0xffff'ffffu);
		Ovl.OffsetHigh = DWORD(FileOffset >> 32);

		DWORD dwNumberOfBytesRead = 0;
		BOOL  Success			  = ::ReadFile(m_FileHandle, Data, DWORD(NumberOfBytesToRead), &dwNumberOfBytesRead, &Ovl);
		if (Success)
		{
			BytesRead = size_t(dwNumberOfBytesRead);
		}
		else
		{
			Error = zen::GetLastError();
		}
#else
		static_assert(sizeof(off_t) >= sizeof(uint64_t), "sizeof(off_t) does not support large files");
		int		Fd		   = int(uintptr_t(m_FileHandle));
		ssize_t ReadResult = pread(Fd, Data, NumberOfBytesToRead, FileOffset);
		if (ReadResult != -1)
		{
			BytesRead = size_t(ReadResult);
		}
		else
		{
			Error = zen::GetLastError();
		}
#endif

		if (Error || (BytesRead != NumberOfBytesToRead))
		{
			std::error_code DummyEc;
			throw std::system_error(std::error_code(Error, std::system_category()),
									fmt::format("ReadFile/pread failed (offset {:#x}, size {:#x}) file: '{}' (size {:#x})",
												FileOffset,
												NumberOfBytesToRead,
												PathFromHandle(m_FileHandle, DummyEc),
												FileSizeFromHandle(m_FileHandle)));
		}

		BytesToRead -= NumberOfBytesToRead;
		FileOffset += NumberOfBytesToRead;
		Data = reinterpret_cast<uint8_t*>(Data) + NumberOfBytesToRead;
	}
}

IoBuffer
BasicFile::ReadAll()
{
	if (const uint64_t Size = FileSize())
	{
		IoBuffer Buffer(Size);
		Read(Buffer.MutableData(), Size, 0);
		return Buffer;
	}
	else
	{
		return {};
	}
}

void
BasicFile::StreamFile(std::function<void(const void* Data, uint64_t Size)>&& ChunkFun)
{
	StreamByteRange(0, FileSize(), std::move(ChunkFun));
}

void
BasicFile::StreamByteRange(uint64_t FileOffset, uint64_t Size, std::function<void(const void* Data, uint64_t Size)>&& ChunkFun)
{
	const uint64_t ChunkSize = 128 * 1024;
	IoBuffer	   ReadBuffer{ChunkSize};
	void*		   BufferPtr = ReadBuffer.MutableData();

	uint64_t RemainBytes   = Size;
	uint64_t CurrentOffset = FileOffset;

	while (RemainBytes)
	{
		const uint64_t ThisChunkBytes = zen::Min(ChunkSize, RemainBytes);

		Read(BufferPtr, ThisChunkBytes, CurrentOffset);

		ChunkFun(BufferPtr, ThisChunkBytes);

		CurrentOffset += ThisChunkBytes;
		RemainBytes -= ThisChunkBytes;
	}
}

uint64_t
BasicFile::Write(CompositeBuffer Data, uint64_t FileOffset, std::error_code& Ec)
{
	uint64_t WrittenBytes = 0;
	for (const SharedBuffer& Buffer : Data.GetSegments())
	{
		MemoryView BlockView = Buffer.GetView();
		Write(BlockView, FileOffset + WrittenBytes, Ec);

		if (Ec)
		{
			return WrittenBytes;
		}

		WrittenBytes += BlockView.GetSize();
	}

	return WrittenBytes;
}

void
BasicFile::Write(MemoryView Data, uint64_t FileOffset, std::error_code& Ec)
{
	Write(Data.GetData(), Data.GetSize(), FileOffset, Ec);
}

void
BasicFile::Write(const void* Data, uint64_t Size, uint64_t FileOffset, std::error_code& Ec)
{
	Ec.clear();

	const uint64_t MaxChunkSize = 2u * 1024 * 1024 * 1024;

	while (Size)
	{
		const uint64_t NumberOfBytesToWrite = Min(Size, MaxChunkSize);

#if ZEN_PLATFORM_WINDOWS
		OVERLAPPED Ovl{};

		Ovl.Offset	   = DWORD(FileOffset & 0xffff'ffffu);
		Ovl.OffsetHigh = DWORD(FileOffset >> 32);

		DWORD dwNumberOfBytesWritten = 0;

		BOOL Success = ::WriteFile(m_FileHandle, Data, DWORD(NumberOfBytesToWrite), &dwNumberOfBytesWritten, &Ovl);
#else
		static_assert(sizeof(off_t) >= sizeof(uint64_t), "sizeof(off_t) does not support large files");
		int	 Fd			  = int(uintptr_t(m_FileHandle));
		int	 BytesWritten = pwrite(Fd, Data, NumberOfBytesToWrite, FileOffset);
		bool Success	  = (BytesWritten > 0);
#endif

		if (!Success)
		{
			Ec = MakeErrorCodeFromLastError();

			return;
		}

		Size -= NumberOfBytesToWrite;
		FileOffset += NumberOfBytesToWrite;
		Data = reinterpret_cast<const uint8_t*>(Data) + NumberOfBytesToWrite;
	}
}

void
BasicFile::Write(MemoryView Data, uint64_t FileOffset)
{
	Write(Data.GetData(), Data.GetSize(), FileOffset);
}

void
BasicFile::Write(const void* Data, uint64_t Size, uint64_t Offset)
{
	std::error_code Ec;
	Write(Data, Size, Offset, Ec);

	if (Ec)
	{
		std::error_code Dummy;
		throw std::system_error(Ec, fmt::format("Failed to write to file '{}'", zen::PathFromHandle(m_FileHandle, Dummy)));
	}
}

void
BasicFile::WriteAll(IoBuffer Data, std::error_code& Ec)
{
	Write(Data.Data(), Data.Size(), 0, Ec);
}

void
BasicFile::Flush()
{
#if ZEN_PLATFORM_WINDOWS
	FlushFileBuffers(m_FileHandle);
#else
	int Fd = int(uintptr_t(m_FileHandle));
	fsync(Fd);
#endif
}

uint64_t
BasicFile::FileSize()
{
#if ZEN_PLATFORM_WINDOWS
	ULARGE_INTEGER liFileSize;
	liFileSize.LowPart = ::GetFileSize(m_FileHandle, &liFileSize.HighPart);
	if (liFileSize.LowPart == INVALID_FILE_SIZE)
	{
		int Error = zen::GetLastError();
		if (Error)
		{
			std::error_code Dummy;
			ThrowSystemError(Error, fmt::format("Failed to get file size from file '{}'", PathFromHandle(m_FileHandle, Dummy)));
		}
	}
	return uint64_t(liFileSize.QuadPart);
#else
	int Fd = int(uintptr_t(m_FileHandle));
	static_assert(sizeof(decltype(stat::st_size)) == sizeof(uint64_t), "fstat() doesn't support large files");
	struct stat Stat;
	if (fstat(Fd, &Stat) == -1)
	{
		std::error_code Dummy;
		ThrowSystemError(GetLastError(), fmt::format("Failed to get file size from file '{}'", PathFromHandle(m_FileHandle, Dummy)));
	}
	return uint64_t(Stat.st_size);
#endif
}

uint64_t
BasicFile::FileSize(std::error_code& Ec)
{
#if ZEN_PLATFORM_WINDOWS
	ULARGE_INTEGER liFileSize;
	liFileSize.LowPart = ::GetFileSize(m_FileHandle, &liFileSize.HighPart);
	if (liFileSize.LowPart == INVALID_FILE_SIZE)
	{
		int Error = zen::GetLastError();
		if (Error)
		{
			Ec = MakeErrorCode(Error);
			return 0;
		}
	}
	return uint64_t(liFileSize.QuadPart);
#else
	int Fd = int(uintptr_t(m_FileHandle));
	static_assert(sizeof(decltype(stat::st_size)) == sizeof(uint64_t), "fstat() doesn't support large files");
	struct stat Stat;
	if (fstat(Fd, &Stat) == -1)
	{
		Ec = MakeErrorCodeFromLastError();
		return 0;
	}
	return uint64_t(Stat.st_size);
#endif
}

void
BasicFile::SetFileSize(uint64_t FileSize)
{
#if ZEN_PLATFORM_WINDOWS
	LARGE_INTEGER liFileSize;
	liFileSize.QuadPart = FileSize;
	BOOL OK				= ::SetFilePointerEx(m_FileHandle, liFileSize, 0, FILE_BEGIN);
	if (OK == FALSE)
	{
		int Error = zen::GetLastError();
		if (Error)
		{
			std::error_code Dummy;
			ThrowSystemError(Error,
							 fmt::format("Failed to set file pointer to {} for file {}", FileSize, PathFromHandle(m_FileHandle, Dummy)));
		}
	}
	OK = ::SetEndOfFile(m_FileHandle);
	if (OK == FALSE)
	{
		int Error = zen::GetLastError();
		if (Error)
		{
			std::error_code Dummy;
			ThrowSystemError(Error,
							 fmt::format("Failed to set end of file to {} for file {}", FileSize, PathFromHandle(m_FileHandle, Dummy)));
		}
	}
#elif ZEN_PLATFORM_MAC
	int Fd = int(intptr_t(m_FileHandle));
	if (ftruncate(Fd, (off_t)FileSize) < 0)
	{
		int Error = zen::GetLastError();
		if (Error)
		{
			std::error_code Dummy;
			ThrowSystemError(Error,
							 fmt::format("Failed to set truncate file to {} for file {}", FileSize, PathFromHandle(m_FileHandle, Dummy)));
		}
	}
#else
	int Fd = int(intptr_t(m_FileHandle));
	if (ftruncate64(Fd, (off64_t)FileSize) < 0)
	{
		int Error = zen::GetLastError();
		if (Error)
		{
			std::error_code Dummy;
			ThrowSystemError(Error,
							 fmt::format("Failed to set truncate file to {} for file {}", FileSize, PathFromHandle(m_FileHandle, Dummy)));
		}
	}
	if (FileSize > 0)
	{
		int Error = posix_fallocate64(Fd, 0, (off64_t)FileSize);
		if (Error)
		{
			std::error_code Dummy;
			ThrowSystemError(Error,
							 fmt::format("Failed to allocate space of {} for file {}", FileSize, PathFromHandle(m_FileHandle, Dummy)));
		}
	}
#endif
}

void
BasicFile::Attach(void* Handle)
{
	ZEN_ASSERT(Handle != nullptr);
	ZEN_ASSERT(m_FileHandle == nullptr);
	m_FileHandle = Handle;
}

void*
BasicFile::Detach()
{
	void* FileHandle = m_FileHandle;
	m_FileHandle	 = 0;
	return FileHandle;
}

//////////////////////////////////////////////////////////////////////////

TemporaryFile::~TemporaryFile()
{
	Close();
}

void
TemporaryFile::Close()
{
	if (m_FileHandle)
	{
#if ZEN_PLATFORM_WINDOWS
		// Mark file for deletion when final handle is closed

		FILE_DISPOSITION_INFO Fdi{.DeleteFile = TRUE};

		SetFileInformationByHandle(m_FileHandle, FileDispositionInfo, &Fdi, sizeof Fdi);
#else
		std::error_code		  Ec;
		std::filesystem::path FilePath = zen::PathFromHandle(m_FileHandle, Ec);
		if (!Ec)
		{
			unlink(FilePath.c_str());
		}
#endif

		BasicFile::Close();
	}
}

void
TemporaryFile::CreateTemporary(std::filesystem::path TempDirName, std::error_code& Ec)
{
	StringBuilder<64> TempName;
	Oid::NewOid().ToString(TempName);

	m_TempPath = TempDirName / TempName.c_str();

	Open(m_TempPath, BasicFile::Mode::kTruncateDelete, Ec);
}

void
TemporaryFile::MoveTemporaryIntoPlace(std::filesystem::path FinalFileName, std::error_code& Ec)
{
	// We intentionally call the base class Close() since otherwise we'll end up
	// deleting the temporary file
	BasicFile::Close();

	std::filesystem::rename(m_TempPath, FinalFileName, Ec);
}

//////////////////////////////////////////////////////////////////////////

void
TemporaryFile::SafeWriteFile(std::filesystem::path Path, MemoryView Data)
{
	TemporaryFile	TempFile;
	std::error_code Ec;
	TempFile.CreateTemporary(Path.parent_path(), Ec);
	if (Ec)
	{
		throw std::system_error(Ec, fmt::format("Failed to create temp file for file at '{}'", Path));
	}
	TempFile.Write(Data, 0);
	TempFile.MoveTemporaryIntoPlace(Path, Ec);
	if (Ec)
	{
		throw std::system_error(Ec, fmt::format("Failed to move temp file '{}' to '{}'", TempFile.GetPath(), Path));
	}
}

//////////////////////////////////////////////////////////////////////////

LockFile::LockFile()
{
}

LockFile::~LockFile()
{
#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
	int Fd = int(intptr_t(m_FileHandle));
	flock(Fd, LOCK_UN | LOCK_NB);
#endif
}

void
LockFile::Create(std::filesystem::path FileName, CbObject Payload, std::error_code& Ec)
{
#if ZEN_PLATFORM_WINDOWS
	Ec.clear();

	const DWORD dwCreationDisposition = CREATE_ALWAYS;
	DWORD		dwDesiredAccess		  = GENERIC_READ | GENERIC_WRITE | DELETE;
	const DWORD dwShareMode			  = FILE_SHARE_READ;
	const DWORD dwFlagsAndAttributes  = FILE_ATTRIBUTE_NORMAL | FILE_FLAG_DELETE_ON_CLOSE;
	HANDLE		hTemplateFile		  = nullptr;

	HANDLE FileHandle = CreateFile(FileName.c_str(),
								   dwDesiredAccess,
								   dwShareMode,
								   /* lpSecurityAttributes */ nullptr,
								   dwCreationDisposition,
								   dwFlagsAndAttributes,
								   hTemplateFile);

	if (FileHandle == INVALID_HANDLE_VALUE)
	{
		Ec = zen::MakeErrorCodeFromLastError();

		return;
	}
#elif ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC
	int Fd = open(FileName.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0666);
	if (Fd < 0)
	{
		Ec = zen::MakeErrorCodeFromLastError();
		return;
	}
	fchmod(Fd, 0666);

	int LockRet = flock(Fd, LOCK_EX | LOCK_NB);
	if (LockRet < 0)
	{
		Ec = zen::MakeErrorCodeFromLastError();
		close(Fd);
		return;
	}

	void* FileHandle = (void*)uintptr_t(Fd);
#endif

	m_FileHandle = FileHandle;

	BasicFile::Write(Payload.GetBuffer(), 0, Ec);
}

void
LockFile::Update(CbObject Payload, std::error_code& Ec)
{
	BasicFile::Write(Payload.GetBuffer(), 0, Ec);
}

//////////////////////////////////////////////////////////////////////////

BasicFileBuffer::BasicFileBuffer(BasicFile& Base, uint64_t BufferSize)
: m_Base(Base)
, m_Buffer(nullptr)
, m_BufferSize(BufferSize)
, m_Size(Base.FileSize())
, m_BufferStart(0)
, m_BufferEnd(0)
{
	m_Buffer = (uint8_t*)Memory::Alloc(m_BufferSize);
}

BasicFileBuffer::~BasicFileBuffer()
{
	Memory::Free(m_Buffer);
}

void
BasicFileBuffer::Read(void* Data, uint64_t Size, uint64_t FileOffset)
{
	if (m_Buffer == nullptr || (Size > m_BufferSize) || (FileOffset + Size > m_Size))
	{
		m_Base.Read(Data, Size, FileOffset);
		return;
	}
	uint8_t* WritePtr = ((uint8_t*)Data);
	uint64_t Begin	  = FileOffset;
	uint64_t End	  = FileOffset + Size;
	if (FileOffset <= m_BufferStart)
	{
		if (End > m_BufferStart)
		{
			uint64_t Count = Min(m_BufferEnd, End) - m_BufferStart;
			memcpy(WritePtr + End - Count - FileOffset, m_Buffer, Count);
			End -= Count;
			if (Begin == End)
			{
				return;
			}
		}
	}
	else if (FileOffset < m_BufferEnd)
	{
		uint64_t Count = Min(m_BufferEnd, End) - FileOffset;
		memcpy(WritePtr + Begin - FileOffset, m_Buffer + Begin - m_BufferStart, Count);
		Begin += Count;
		if (Begin == End)
		{
			return;
		}
	}
	m_BufferStart = Begin;
	m_BufferEnd	  = Min(Begin + m_BufferSize, m_Size);
	m_Base.Read(m_Buffer, m_BufferEnd - m_BufferStart, m_BufferStart);
	uint64_t Count = Min(m_BufferEnd, End) - m_BufferStart;
	memcpy(WritePtr + Begin - FileOffset, m_Buffer, Count);
	ZEN_ASSERT(Begin + Count == End);
}

MemoryView
BasicFileBuffer::MakeView(uint64_t Size, uint64_t FileOffset)
{
	if (FileOffset < m_BufferStart || (FileOffset + Size) > m_BufferEnd)
	{
		if (m_Buffer == nullptr || (Size > m_BufferSize) || (FileOffset + Size > m_Size))
		{
			return {};
		}
		m_BufferStart = FileOffset;
		m_BufferEnd	  = Min(m_BufferStart + m_BufferSize, m_Size);
		m_Base.Read(m_Buffer, m_BufferEnd - m_BufferStart, m_BufferStart);
	}
	return MemoryView(m_Buffer + (FileOffset - m_BufferStart), Size);
}

//////////////////////////////////////////////////////////////////////////

BasicFileWriter::BasicFileWriter(BasicFile& Base, uint64_t BufferSize)
: m_Base(Base)
, m_Buffer(nullptr)
, m_BufferSize(BufferSize)
, m_BufferStart(0)
, m_BufferEnd(0)
{
	m_Buffer = (uint8_t*)Memory::Alloc(m_BufferSize);
}

BasicFileWriter::~BasicFileWriter()
{
	Flush();
	Memory::Free(m_Buffer);
}

void
BasicFileWriter::Write(const void* Data, uint64_t Size, uint64_t FileOffset)
{
	if (m_Buffer == nullptr || (Size >= m_BufferSize))
	{
		m_Base.Write(Data, Size, FileOffset);
		return;
	}

	// Note that this only supports buffering of sequential writes!

	if (FileOffset != m_BufferEnd)
	{
		Flush();
		m_BufferStart = m_BufferEnd = FileOffset;
	}

	const uint8_t* DataPtr = (const uint8_t*)Data;
	while (Size)
	{
		const uint64_t RemainingBufferCapacity = m_BufferStart + m_BufferSize - m_BufferEnd;
		const uint64_t BlockWriteBytes		   = Min(RemainingBufferCapacity, Size);
		const uint64_t BufferWriteOffset	   = FileOffset - m_BufferStart;

		ZEN_ASSERT_SLOW(BufferWriteOffset < m_BufferSize);
		ZEN_ASSERT_SLOW((BufferWriteOffset + BlockWriteBytes) <= m_BufferSize);

		memcpy(m_Buffer + BufferWriteOffset, DataPtr, BlockWriteBytes);

		Size -= BlockWriteBytes;
		m_BufferEnd += BlockWriteBytes;
		FileOffset += BlockWriteBytes;
		DataPtr += BlockWriteBytes;

		if ((m_BufferEnd - m_BufferStart) == m_BufferSize)
		{
			Flush();
		}
	}
}

void
BasicFileWriter::Flush()
{
	const uint64_t BufferedBytes = m_BufferEnd - m_BufferStart;

	if (BufferedBytes == 0)
		return;

	const uint64_t WriteOffset = m_BufferStart;
	m_BufferStart			   = m_BufferEnd;

	m_Base.Write(m_Buffer, BufferedBytes, WriteOffset);
}

//////////////////////////////////////////////////////////////////////////

/*
		___________              __
		\__    ___/___   _______/  |_  ______
		  |    |_/ __ \ /  ___/\   __\/  ___/
		  |    |\  ___/ \___ \  |  |  \___ \
		  |____| \___  >____  > |__| /____  >
					 \/     \/            \/
*/

#if ZEN_WITH_TESTS

TEST_CASE("BasicFile")
{
	ScopedCurrentDirectoryChange _;

	BasicFile File1;
	CHECK_THROWS(File1.Open("zonk", BasicFile::Mode::kRead));
	CHECK_NOTHROW(File1.Open("zonk", BasicFile::Mode::kTruncate));
	CHECK_NOTHROW(File1.Write("abcd", 4, 0));
	CHECK(File1.FileSize() == 4);
	{
		IoBuffer Data = File1.ReadAll();
		CHECK(Data.Size() == 4);
		CHECK_EQ(memcmp(Data.Data(), "abcd", 4), 0);
	}
	CHECK_NOTHROW(File1.Write("efgh", 4, 2));
	CHECK(File1.FileSize() == 6);
	{
		IoBuffer Data = File1.ReadAll();
		CHECK(Data.Size() == 6);
		CHECK_EQ(memcmp(Data.Data(), "abefgh", 6), 0);
	}
}

TEST_CASE("TemporaryFile")
{
	ScopedCurrentDirectoryChange _;

	SUBCASE("DeleteOnClose")
	{
		TemporaryFile	TmpFile;
		std::error_code Ec;
		TmpFile.CreateTemporary(std::filesystem::current_path(), Ec);
		CHECK(!Ec);
		CHECK(std::filesystem::exists(TmpFile.GetPath()));
		TmpFile.Close();
		CHECK(std::filesystem::exists(TmpFile.GetPath()) == false);
	}

	SUBCASE("MoveIntoPlace")
	{
		TemporaryFile	TmpFile;
		std::error_code Ec;
		TmpFile.CreateTemporary(std::filesystem::current_path(), Ec);
		CHECK(!Ec);
		std::filesystem::path TempPath	= TmpFile.GetPath();
		std::filesystem::path FinalPath = std::filesystem::current_path() / "final";
		CHECK(std::filesystem::exists(TempPath));
		TmpFile.MoveTemporaryIntoPlace(FinalPath, Ec);
		CHECK(!Ec);
		CHECK(std::filesystem::exists(TempPath) == false);
		CHECK(std::filesystem::exists(FinalPath));
	}
}

TEST_CASE("BasicFileBuffer")
{
	ScopedCurrentDirectoryChange _;
	{
		BasicFile			   File1;
		const std::string_view Data = "0123456789abcdef";
		File1.Open("buffered", BasicFile::Mode::kTruncate);
		for (uint32_t I = 0; I < 16; ++I)
		{
			File1.Write(Data.data(), Data.size(), I * Data.size());
		}
	}
	SUBCASE("EvenBuffer")
	{
		BasicFile File1;
		File1.Open("buffered", BasicFile::Mode::kRead);
		BasicFileBuffer File1Buffer(File1, 16);
		// Non-primed
		{
			char Buffer[16] = {0};
			File1Buffer.Read(Buffer, 16, 1 * 16);
			std::string_view Verify(Buffer, 16);
			CHECK(Verify == "0123456789abcdef");
		}
		// Primed
		{
			char Buffer[16] = {0};
			File1Buffer.Read(Buffer, 16, 1 * 16);
			std::string_view Verify(Buffer, 16);
			CHECK(Verify == "0123456789abcdef");
		}
	}
	SUBCASE("UnevenBuffer")
	{
		BasicFile File1;
		File1.Open("buffered", BasicFile::Mode::kRead);
		BasicFileBuffer File1Buffer(File1, 16);
		// Non-primed
		{
			char Buffer[16] = {0};
			File1Buffer.Read(Buffer, 16, 7);
			std::string_view Verify(Buffer, 16);
			CHECK(Verify == "789abcdef0123456");
		}
		// Primed
		{
			char Buffer[16] = {0};
			File1Buffer.Read(Buffer, 16, 7);
			std::string_view Verify(Buffer, 16);
			CHECK(Verify == "789abcdef0123456");
		}
	}
	SUBCASE("BiggerThanBuffer")
	{
		BasicFile File1;
		File1.Open("buffered", BasicFile::Mode::kRead);
		BasicFileBuffer File1Buffer(File1, 16);
		char			Buffer[17] = {0};
		File1Buffer.Read(Buffer, 17, 0 * 16);
		std::string_view Verify(Buffer, 17);
		CHECK(Verify == "0123456789abcdef0");
	}
	SUBCASE("InsideBuffer")
	{
		BasicFile File1;
		File1.Open("buffered", BasicFile::Mode::kRead);
		BasicFileBuffer File1Buffer(File1, 16);
		char			Buffer[16] = {0};
		File1Buffer.Read(Buffer, 16, 0 * 16);

		File1Buffer.Read(Buffer, 8, 2);
		std::string_view Verify(Buffer, 8);
		CHECK(Verify == "23456789");
	}
	SUBCASE("BeginningOfBuffer")
	{
		BasicFile File1;
		File1.Open("buffered", BasicFile::Mode::kRead);
		BasicFileBuffer File1Buffer(File1, 16);
		char			Buffer[16] = {0};
		File1Buffer.Read(Buffer, 16, 8);

		File1Buffer.Read(Buffer, 8, 8);
		std::string_view Verify(Buffer, 8);
		CHECK(Verify == "89abcdef");
	}
	SUBCASE("EndOfBuffer")
	{
		BasicFile File1;
		File1.Open("buffered", BasicFile::Mode::kRead);
		BasicFileBuffer File1Buffer(File1, 16);
		char			Buffer[16] = {0};
		File1Buffer.Read(Buffer, 16, 0 * 16);

		File1Buffer.Read(Buffer, 8, 8);
		std::string_view Verify(Buffer, 8);
		CHECK(Verify == "89abcdef");
	}
	SUBCASE("OverEnd")
	{
		BasicFile File1;
		File1.Open("buffered", BasicFile::Mode::kRead);
		BasicFileBuffer File1Buffer(File1, 16);
		char			Buffer[16] = {0};
		File1Buffer.Read(Buffer, 16, 0 * 16);

		File1Buffer.Read(Buffer, 16, 8);
		std::string_view Verify(Buffer, 16);
		CHECK(Verify == "89abcdef01234567");
	}
	SUBCASE("OverBegin")
	{
		BasicFile File1;
		File1.Open("buffered", BasicFile::Mode::kRead);
		BasicFileBuffer File1Buffer(File1, 16);
		char			Buffer[16] = {0};
		File1Buffer.Read(Buffer, 16, 1 * 16);

		File1Buffer.Read(Buffer, 16, 8);
		std::string_view Verify(Buffer, 16);
		CHECK(Verify == "89abcdef01234567");
	}
	SUBCASE("EndOfFile")
	{
		BasicFile File1;
		File1.Open("buffered", BasicFile::Mode::kRead);
		BasicFileBuffer File1Buffer(File1, 16);
		char			Buffer[16] = {0};
		File1Buffer.Read(Buffer, 16, 0 * 16);

		File1Buffer.Read(Buffer, 8, 256 - 8);
		std::string_view Verify(Buffer, 8);
		CHECK(Verify == "89abcdef");
	}
}

void
basicfile_forcelink()
{
}

#endif

}  // namespace zen
